Alamofire网络库进阶教程
本教程的第二部分,您将会增加以下功能:
- 照片查看器
- 查看评论以及其他信息的功能
- 下载照片功能,附带有一个圆列进度条
- 优化网络访问以及图片缓存
- 下拉刷新操作
让我们开始吧 您可以使用您在教程第一部分所完成的项目来开始本章教程。但是如果您跳过了第一部分的教程或者对自己的项目没有信心的话,那么您也可以使用我们提供的标准项目。
提示: 如果您跳过了第一部分的教程,那么请不要忘记您首先应当从 500px.com 网站上获取消费者密钥,然后在Five100px.swift中用其替换必要的部分。关于如何获取该密钥,以及在何处使用它,都在本教程的第一部分:Alamofire 网络库基础教程中有详细说明。 生成并运行起始项目,以确定我们应用运行正常。图片预览功能能够正常工作,但是单击图片并不会将其以全屏打开。这就是我们所要解决的问题!
创建图片查看器
说句老实话,范型可以说是包括 Swift 在内的高级编程语言中最强大的特性之一。一般情况下,在我们这个项目中最好使用范型这个功能。
打开Five100px.swift,然后在文件顶部,即import Alamofire语句下方添加以下代码:
public protocol ResponseObjectSerializable {
init?(response: NSHTTPURLResponse, representation: AnyObject)
}
extension Alamofire.Request {
public func responseObject<T: ResponseObjectSerializable>(completionHandler: Response<T, NSError> -> Void) -> Self {
let responseSerializer = ResponseSerializer<T, NSError> { request, response, data, error in
let JSONResponseSerializer = Request.JSONResponseSerializer(options: .AllowFragments)
let result = JSONResponseSerializer.serializeResponse(request, response, data, error)
switch result {
case .Success(let value):
if let response = response, responseObject = T(response: response, representation: value) {
return .Success(responseObject)
} else {
let failureReason = "无法将JSON解析为\(value)回应对象"
let error = Error.errorWithCode(.JSONSerializationFailed, failureReason: failureReason)
return .Failure(error)
}
case .Failure(let error):
return .Failure(error)
}
}
return response(responseSerializer: responseSerializer, completionHandler: completionHandler)
}
}
在上述的代码中,我们再一次给 Alamofire 创建了一个扩展,添加了新的响应序列化方法。这次,我们添加了.responseObject()函数。作为一个通用函数,它能够序列化所有符合ResponseObjectSerializable协议的数据对象。
这意味着,如果我们定义一个含有init?(response:representation:)初始化方法的新类,那么 Alamofire 就能够自行从服务器返回该类型的对象。这时候,我们已经将序列化逻辑封装进了自定义类的内部。哈哈,是不是一个很赞的面向对象设计?
图片查看器使用的是PhotoInfo类,这个类遵守了ResponseObjectSerializable协议,并实现了所需的方法。不过您仍需要让这个类正式遵守ResponseObjectSerializable协议。
打开Five100px.swift,并且修改PhotoInfo类的声明来让其明确遵守ResponseObjectSerializable协议,如下所示:
class PhotoInfo: NSObject, ResponseObjectSerializable {
提示: 虽然毋需详细了解representation参数在PhotoInfo对象中是如何序列化的,但是感兴趣的读者可以去浏览required init(response:representation:)方法来了解其工作原理。 打开PhotoViewerViewController.swift,注意不是PhotoBrowserCollectionViewController.swift,然后在文件顶部加入一个必要的导入声明:
import Alamofire接着,在viewDidLoad()方法内的底部加入以下代码:
loadPhoto()您会得到一个找不到loadPhoto()的错误,但是不必担心,我们接下来就要实现这个函数。
仍然是在同一个文件当中,在setupView()方法前加入以下代码:
func loadPhoto() {
Alamofire.request(Five100px.Router.PhotoInfo(self.photoID, .Large)).validate().responseObject() {
(response: Response<PhotoInfo, NSError>) in
guard response.result.error == nil else { return }
let photoInfo = response.result.value
self.photoInfo = photoInfo
dispatch_async(dispatch_get_main_queue()) {
self.addButtomBar()
self.title = photoInfo?.url
}
Alamofire.request(.GET, photoInfo!.url).validate().responseImage() {
response in
guard let image = response.result.value else { return }
self.imageView.image = image
self.imageView.frame = self.centerFrameFromImage(image)
self.spinner.stopAnimating()
self.centerScrollViewContents()
}
}
}
这时,我们在其他 Alamofire 请求的完成处理方法中发出了 Alamofire 请求。第一个请求接收到了一个 JSON 响应数据,然后它使用我们新建的通用响应序列化方法,在JSON 数据之外创建了一个PhotoInfo实例。
(response: Response<PhotoInfo, NSError>) in是响应序列化方法的参数。我们可以从 Response 结构体中获取到所需的数据。这个参数中明确声明了我们的PhotoInfo实例,因此通用序列化方法将自行初始化,并返回该类型的一个对象,其包含有图片 URL。
第二个 Alamofire 请求使用您先前创建过的图片序列化方法,将NSData转化为UIImage,以便之后我们在图片视图中显示它。
注意: 我们不在这里使用路由,因为我们已经有了图片的绝对 URL 地址,我们无需自行构造 URL。
在请求响应对象之前调用的.validate()函数是另一个易用的 Alamofire 特性。将其与请求和响应链接,以确认响应的状态码在默认可接受的范围(200到299)内。如果认证失败,响应处理方法将出现一个相关错误,您可以在完成处理方法中处理这个错误。
即使没有发生错误,完成处理方法仍然还是会被调用。参数中NSError的值让我们能够在自定义方法中响应这个错误。
生成并运行您的应用,单击其中一副图片后您应当看到它将覆盖全屏幕,如下所示:
好的!图片查看器正常工作了,双击该图片可以放大图片。
当类型安全的通用响应序列化方法初始化PhotoInfo的时候,您不仅仅只是设置了id和url属性,还有一些属性您并没有看见。
单击应用左下方的Menu按钮,然后您就可以看到该照片的详细信息:
单击屏幕上的任意位置就可以关闭照片详细信息。
如果您对 500px.com很熟悉的话,您就知道用户往往会在网站上给极佳的照片留下很多评论信息。现在既然我们的图片查看器正常工作了,那么现在我们就要开始搭建评论查看器了。
为显示注释创建集合序列化方法
对于有评论的图片来说,图片查看器会显示一个包含评论数的评论按钮。单击这个评论按钮会弹出一个评论列表。
打开Five100px.swift,然后在import Alamofire声明下添加以下代码:
public protocol ResponseCollectionSerializable {
static func collection(response response: NSHTTPURLResponse, representation: AnyObject) -> [Self]
}
extension Alamofire.Request {
public func responseCollection<T: ResponseCollectionSerializable>(completionHandler: Response<[T], NSError> -> Void) -> Self {
let responseSerializer = ResponseSerializer<[T], NSError> { request, response, data, error in
guard error == nil else { return .Failure(error!) }
let JSONSerializer = Request.JSONResponseSerializer(options: .AllowFragments)
let result = JSONSerializer.serializeResponse(request, response, data, error)
switch result {
case .Success(let value):
if let response = response {
return .Success(T.collection(response: response, representation: value))
} else {
let failureReason = "响应集合无法解析,因为服务器响应为空"
let error = Error.errorWithCode(.JSONSerializationFailed, failureReason: failureReason)
return .Failure(error)
}
case .Failure(let error):
return .Failure(error)
}
}
return response(responseSerializer: responseSerializer, completionHandler: completionHandler)
}
}
这段代码看起来很眼熟,它和我们之前创建的通用响应序列化方法相似。
唯一的不同点是,这个协议定义了返回集合的一个类方法(而不是单个元素),在本例中,返回的是[self]。完成处理方法将集合作为Response中的参数,即[T],接着调用类型上的collection方法而不是调用初始化方法。
仍然是在同一个文件当中,将整个Comment类替换为以下形式:
final class Comment: ResponseCollectionSerializable {
static func collection(response response: NSHTTPURLResponse, representation: AnyObject) -> [Comment] {
var comments = [Comment]()
guard let represences = representation.valueForKeyPath("comments") as? [NSDictionary] else { return comments }
represences.forEach { comments.append(Comment(JSON: $0)) }
return comments
}
let userFullname: String
let userPictureURL: String
let commentBody: String
init(JSON: AnyObject) {
userFullname = JSON.valueForKeyPath("user.fullname") as! String
userPictureURL = JSON.valueForKeyPath("user.userpic_url") as! String
commentBody = JSON.valueForKeyPath("body") as! String
}
}
这段代码让Comment遵守ResponseCollectionSerializable协议,因此它将和上面的响应序列化方法协同工作。
现在您需要做的就是使用它。打开PhotoCommentsViewController.swift然后在文件顶部加入以下必要的导入声明:
import Alamofire
现在在viewDidLoad()方法中的底部加入以下代码:
Alamofire.request(Five100px.Router.Comments(photoID, 1)).validate().responseCollection() {
(response: Response<[Comment], NSError>) in
guard response.result.error == nil else { return }
self.comments = response.result.value
self.tableView.reloadData()
}
这段代码使用了您新建的响应序列化方法来解序列化位于Comment集合中响应的NSData,然后将它们保存在属性当中,最后在表视图中重新加载它们。
接下来,向tableView(_:cellForRowAtIndexPath)中添加以下代码(在return cell语句上面):
cell.userFullnameLabel.text = comments![indexPath.row].userFullname
cell.commentLabel.text = comments![indexPath.row].commentBody
cell.userImageView.image = nil
let imageURL = comments![indexPath.row].userPictureURL
Alamofire.request(.GET, imageURL).validate().responseImage() {
response in
if response.result.error == nil {
if response.request?.URLString == imageURL {
cell.userImageView.image = response.result.value!
}
}
}
这段代码在表视图单元格中显示了评论信息,同样它还接连提交了第二个 Alamofire 请求来加载图片(这和我们在教程第一部分所做的相类似)。
生成并运行您的应用,找到一个有评论的图片。您可以通过评论图标上的数字来了解该图片的评论数目。单击评论按钮,然后该图片的评论界面就会显示出来,如下所示:

现在,您可能已经发现了几张您想下载的图片(也有可能是几百张哈),下一节我们将带领大家如何实现下载功能。
显示下载进度
图片查看器的底栏中间有一个动作按钮。它显示出一个UIActionSheet控件来让您选择是否下载照片,但是现在它还没有任何功能。到目前为止,我们所做的仅仅只是从 500px.com 加载到内存中。那么我们要如何下载并保存文件呢?
打开PhotoViewerViewController.swift,将空函数downloadPhoto()用以下代码替换:
func downloadPhoto() {
// 1
Alamofire.request(Five100px.Router.PhotoInfo(photoInfo!.id, .XLarge)).validate().responseJSON() {
response in
guard response.result.error == nil else { return }
guard let jsonDictionary = response.result.value as? NSDictionary else { return }
guard let imageURL = jsonDictionary.valueForKeyPath("photo.image_url") as? String else { return }
// 2
let destination = Alamofire.Request.suggestedDownloadDestination(directory: .DocumentDirectory, domain: .UserDomainMask)
// 3
Alamofire.download(.GET, imageURL, destination: destination)
}
}
}
我们来依次解释一下这些代码的作用:
- 我们首先请求一个新的PhotoInfo,此时请求的是XLarge大小。
- 获取要保存文件的默认存储地址,我们将会将其存放在您应用的 Documents 目录的一个子目录中。该子目录的名字将和服务器建议的名字相同。destination是一个变相的闭包——虽然只是短短的一瞬间
- Alamofire.download(::)方法和Alamofire.request(:)方法有很大的不同。Alamofire.download(::)方法不需要响应处理方法,也不需要响应序列化方法来对数据进行处理。因为它已经知道如何去处理这些数据了,就是把它们保存在硬盘上!destination闭包将返回要保存图片的路径。
生成并运行您的应用,然后找到某个您最喜欢的图片,单击动作按钮,接着单击Download Photo按钮。目前您还不会看到任何的回应,但是回到应用主界面来,然后单击Downloads标签,这时您就可以看到您下载的照片了。
您或许会问了:“为什么不直接使用同一个文件路径来保存呢?”。原因是在我们下载图片之前我们并不知道图片的名字。对于 500px.com 来说,服务器始终只会根据图片的尺寸返回1.jpg、2.jpg、3.jpg、4.jpg或者5.jpg这样的名字。我们不能够将相同名字的图片保存在同样的文件夹内。
我们使用闭包作为Alamofire.download的第三个参数,而不是传递进一个固定的字符串路径。Alamofire 接着就会在一个合适的时间调用该闭包,然后将temporaryURL和NSHTTPURLResponse传递进来作为参数,然后返回一个 URL 的实例,指向硬盘上您有权访问的路径。
试着保存一些图片,然后返回到下载标签,这时候您会发现,诶?!为啥只有一个文件?闹哪样嘛!
这是因为文件名并不是独一无二的,因此我们需要实现自己的命名逻辑。用以下代码替换掉downloadPhoto()方法中的注释 //2 下的语句:
let destination: (NSURL, NSHTTPURLResponse) -> (NSURL) = {
temporaryURL, response in
let directoryURL = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)[0]
return directoryURL.URLByAppendingPathComponent("\(self.photoInfo!.id).\(response.suggestedFilename)")
}
再次说明,let destination是一个闭包,但是我们这次实现了自己的命名逻辑。我们使用在闭包外捕获的图片id,然后将其和服务器建议的名称相连接,这两者之间使用“.”来分隔。
生成并运行,然后现在您就可以保存多个图片了:

文件保存功能目前工作得很好,但是如果给用户显示下载进度的话那岂不是更好?Alamofire 可以很容易地实现显示下载进度指示器。
用以下代码替换掉downloadPhoto()方法中的注释 //3 下的语句:
// 4
let progressIndicatorView = UIProgressView(frame: CGRect(x: 0.0, y: 80.0, width: self.view.bounds.width, height: 10.0))
progressIndicatorView.tintColor = UIColor.blueColor()
self.view.addSubview(progressIndicatorView)
Alamofire.download(.GET, imageURL, destination: destination).progress {
_, totalBytesRead, totalBytesExpectedToRead in
dispatch_async(dispatch_get_main_queue()) {
progressIndicatorView.setProgress(Float(totalBytesRead) / Float(totalBytesExpectedToRead), animated: true)
if totalBytesRead == totalBytesExpectedToRead {
progressIndicatorView.removeFromSuperview()
}
}
}
我们来依次解释一下这些代码的作用:
- 我们使用标准UIProgressView控件来显示图片下载进度。对其进行配置并将其添加到view视图上。
- 借助 Alamofire,我们可以将.progress()和三个参数进行链接。.progress将定期调用一个闭包,而且这三个参数分别是:bytesRead、totalBytesRead以及totalBytesExpectedToRead。
- 简单用totalBytesExpectedToRead除以totalBytesRead,我们就可以得到一个0到1之间的数字,这个数字代表这下载进度。如果下载时间不是瞬时的话,那么这个闭包可能会多次运行。每次运行我们都能够更新屏幕上的进度条。
- 一旦下载结束,我们只需从view 视图上移去进度条。
生成并运行您的应用,找到某个图片并下载它,这是您就可以看到下载进度条。

当下载完成时,进度条消失,因此如果您的网络很快的话那么您很有可能会看不到这个进度条。
我们注意到,downloadPhoto方法仍然是使用在教程第一部分中用过的.resposneJSON()。我们要确保您能够理解响应序列化方法是如何工作的。因此请自行创建一个通用的响应序列化方法.responseObject()来替代.resposneJSON()。如果您想要检查您的解决方案,那么您可以继续向下查看我们是如何解决的:
downloadPhoto()方法开头的几个语句需要变为以下的形式:
Alamofire.request(Five100px.Router.PhotoInfo(photoInfo!.id, .XLarge)).validate().responseObject() {
response in
guard response.result.error == nil else { return }
guard let photoInfo = response.result.value else { return }
let imageURL = photoInfo.url
.
.
.
}
这里您应当使用自定义的响应序列化方法,而不是手动解析 JSON 数据。这样可以让您的代码更为简洁。
优化和刷新
好的,现在是时候来实现下拉刷新功能了。(强迫症患者们总是在不停地刷新……刷新……再刷新,对吧?泪一般的事实)
打开PhotoBrowserCollectionViewController.swift,然后用以下代码替换func handleRefresh():
func handleRefresh() {
refreshControl.beginRefreshing()
self.photos.removeAll()
self.currentPage = 1
self.collectionView!.reloadData()
refreshControl.endRefreshing()
populatePhotos()
}
上述的代码清空您当前的集合(self.photos),然后重置currentPage,最后刷新 UI。
生成并运行您的应用,下拉刷新,这时候您就可以看到新的图片出现了:

当您快速滑动照片浏览页面的时候,您可能会注意到可以将仍然在请求图片的单元送出屏幕。实际上,图片请求在其结束前会一直运行,但是下载的照片以及相关数据则会被丢弃。
此外,当您返回到之前的单元,您就必须还得为显示图片而发送网络请求,即使您刚才下载了那幅图片。我们需要改善这个设计,以防止浪费带宽。
我们可以利用缓存来保存加载过的图像,这样就可以不必再次加载。还有,如果某个单元在网络请求结束前出列了,我们可以取消其所有的网络请求。
打开PhotoBrowserCollectionViewController.swift,然后在let refreshControl语句上添加以下代码:
let imageCache = NSCache()
它创建了一个用于缓存图片的NSCache对象。
接下来,用以下代码替换collectionView(_:cellForItemAtIndexPath:):
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(PhotoBrowserCellIdentifier, forIndexPath: indexPath) as! PhotoBrowserCollectionViewCell
let imageURL = self.photos[self.photos.startIndex.advancedBy(indexPath.item)].url
// 1
cell.request?.cancel()
// 2
if let image = self.imageCache.objectForKey(imageURL) as? UIImage {
cell.imageView.image = image
} else {
// 3
cell.imageView.image = nil
// 4
cell.request = Alamofire.request(.GET, imageURL).validate(contentType: ["image/*"]).responseImage() {
response in
guard let image = response.result.value where response.result.error == nil else { return }
// 5
self.imageCache.setObject(image!, forKey: response.request!.URLString)
// 6
cell.imageView.image = image
}
/*
如果单元格在图片下载完成之前离开了屏幕,那么我们将暂停下载,并且返回一个NSURLErrorDomain (-999: cancelled)对象。这是业界普遍的处理方式。
*/
}
return cell
}
我们来依次解释一下这些代码的作用:
- 出队的单元可能已经有一个连带的 Alamorire 请求。检查这个请求是否相关,也就是说,检查该请求的 URL 是否和要显示的图片 URL 相匹配,否则就取消请求。
- 使用可选值绑定来检查该图片是否有缓存版本。如果有的话,使用该缓存版本而不是再次下载。
- 如果没有相应的缓存版本的话,那么就下载它。然后,出列单元可能已经显示出了另一幅图像。这样子的话,就将其设置为nil,因此当图片在下载时该单元将为空。
- 从服务器下载图片,然后验证返回响应的content-type。如果返回的不是图片,那么返回值就是为error。因此您就不会再对这个无效的图片进行操作了。这里的关键是我们在单元中存储了 Alamofire 请求对象,当网络异步调用返回时使用。
- 如果我们没有接收到错误信息,那么就下载相应的图片,并在随后缓存它。
- 检查单元是否出列以显示新的图片。如果没有的话,则相应地设置单元的图片。
生成并运行您的应用。您会注意到随着在照片浏览器中来回滚动,图像的加载速度快了许多。我们已经砍掉了不必要的请求,并缓存了已下载的图片以便重复使用,这些操作让我们的网络请求优化了不少。良好的网络处理和灵活的用户界面能够提高用户体验的哦!
注意: 某些单元可能会显示为空,而当它全屏显示时又不为空。这并不是您的原因,这是因为 500px.com 并没有这些图片的缩略图。
接下来该何去何从?
这里是本系列教程的完整项目。您会看到本教程没有涉及到的很多 UI 元素的设计细节。
如果您跟随我们的教程完成了学习,那么您现在已经能够很好的使用最受欢迎的第三方库—— Alamofire 了。我们学习了可链接的请求和响应方法、生成自定义响应序列化方法、创建路由并将 URL 参数进行编码、下载文件、使用进度条,以及响应验证。恭喜您获得了一系列成就!当当当!
Alamofire 同样能验证使用不同方案的服务器。同样,它还可以上传文件和流数据(并未在本教程中提到)。但是您目前对 Alamofire 的了解,相信您能够很快了解如何完成这些任务。
Alamofire 目前并没有实现 AFNetworking 的全部功能。但是如果您使用 Swift 来开始一个新项目,那么使用 Alamofire 则是最佳选择,因为它已经涵盖了最常用的网络操作。AFNetworking 的最受欢迎的特征之一——UIKit 扩展还没有在 Alamofire 当中实现。但是这两个库是可以在同一个项目中并存的。
如果您先前已经使用过 AFNetworking,并且受不了没有 UIKit 上的类别方法setImageWithURL的话,那么您可能就还得在您的项目中继续使用 AFNetworking。例如,您可以用 Alamofire 调用服务器的 API,然后使用 AFNetworking 来异步显示图像。 AFNetworking 有一个共享的缓存,这样您无需手动管理缓存或者取消网络请求。